AWSTemplateFormatVersion: '2010-09-09'
Description: >
  NAT Gateway Idle Review / Safe Delete Remediator.
  Purpose-specific, opt-in remediation module for identifying NAT Gateways with low
  CloudWatch data processing activity and optionally deleting only explicitly approved
  NAT Gateways that are no longer referenced by route tables. This template is
  separate from the cost optimization assessment/reporting template.

Parameters:
  ProjectName:
    Type: String
    Default: nat-gateway-idle-review-remediator
    Description: Base name used for created resources.

  TargetRegionsCsv:
    Type: String
    Default: all-enabled
    Description: Comma-separated AWS Regions to scan, or all-enabled to scan all enabled Regions.

  ExcludedRegionsCsv:
    Type: String
    Default: ap-northeast-3
    Description: Comma-separated AWS Regions to exclude from scanning/remediation.

  DryRun:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: When true, only report what would happen. When false, perform eligible actions.

  ActionMode:
    Type: String
    Default: review-only
    AllowedValues:
      - review-only
      - delete
    Description: review-only never changes resources. delete attempts to delete eligible approved NAT Gateways.

  MetricsLookbackDays:
    Type: Number
    Default: 7
    Description: Number of days of CloudWatch NAT Gateway metrics to review.

  LowBytesDailyAverageThreshold:
    Type: Number
    Default: 1048576
    Description: NAT Gateways with average daily bytes below this threshold are considered low activity. Default is 1 MiB/day.

  MinimumNatGatewayAgeDays:
    Type: Number
    Default: 30
    Description: Only NAT Gateways older than this many days are considered delete candidates.

  RequireApprovalTag:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: NAT Gateway deletion requires ApprovalTagKey=ApprovalTagValue when true. Strongly recommended.

  ApprovalTagKey:
    Type: String
    Default: CostOptApproved
    Description: Tag key required when RequireApprovalTag is true.

  ApprovalTagValue:
    Type: String
    Default: 'true'
    Description: Tag value required when RequireApprovalTag is true.

  IgnoreTagKeysCsv:
    Type: String
    Default: CostOptIgnore,DoNotDelete,DoNotModify
    Description: Comma-separated tag keys that prevent deletion when present, regardless of value.

  IgnoreTagPairsCsv:
    Type: String
    Default: CostOptIgnore=true,DoNotDelete=true,DoNotModify=true
    Description: Comma-separated key=value tag pairs that prevent deletion when present.

  RequireNoRouteReferences:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: When true, skip deletion if any route table route still targets the NAT Gateway.

  IncludeNamePrefixesCsv:
    Type: String
    Default: ''
    Description: Optional comma-separated NAT Gateway Name tag prefixes to include. Leave blank to include all.

  ExcludeNamePrefixesCsv:
    Type: String
    Default: ''
    Description: Optional comma-separated NAT Gateway Name tag prefixes to exclude.

  MaxNatGatewaysPerRun:
    Type: Number
    Default: 25
    Description: Safety limit. 0 means no limit.

  ContinueOnAccessDenied:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: Continue scanning other Regions/resources when blocked by IAM/SCP.

  ScheduleEnabled:
    Type: String
    Default: 'false'
    AllowedValues: ['true', 'false']
    Description: Whether to create an enabled EventBridge schedule.

  ScheduleExpression:
    Type: String
    Default: rate(30 days)
    Description: EventBridge schedule expression used when ScheduleEnabled is true.

Conditions:
  IsScheduleEnabled: !Equals [!Ref ScheduleEnabled, 'true']

Resources:
  RemediatorRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${ProjectName}-${AWS::AccountId}-${AWS::Region}-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: NatGatewayIdleReviewPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: DiscoverAndReadEc2
                Effect: Allow
                Action:
                  - ec2:DescribeRegions
                  - ec2:DescribeNatGateways
                  - ec2:DescribeRouteTables
                Resource: '*'
              - Sid: ReadCloudWatchMetrics
                Effect: Allow
                Action:
                  - cloudwatch:GetMetricStatistics
                Resource: '*'
              - Sid: DeleteNatGateway
                Effect: Allow
                Action:
                  - ec2:DeleteNatGateway
                Resource: '*'

  RemediatorFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Ref ProjectName
      Description: Purpose-specific Lambda for NAT Gateway idle review and safe delete.
      Runtime: python3.12
      Handler: index.handler
      Timeout: 900
      MemorySize: 512
      Role: !GetAtt RemediatorRole.Arn
      Environment:
        Variables:
          TARGET_REGIONS_CSV: !Ref TargetRegionsCsv
          EXCLUDED_REGIONS_CSV: !Ref ExcludedRegionsCsv
          DRY_RUN: !Ref DryRun
          ACTION_MODE: !Ref ActionMode
          METRICS_LOOKBACK_DAYS: !Ref MetricsLookbackDays
          LOW_BYTES_DAILY_AVERAGE_THRESHOLD: !Ref LowBytesDailyAverageThreshold
          MINIMUM_NAT_GATEWAY_AGE_DAYS: !Ref MinimumNatGatewayAgeDays
          REQUIRE_APPROVAL_TAG: !Ref RequireApprovalTag
          APPROVAL_TAG_KEY: !Ref ApprovalTagKey
          APPROVAL_TAG_VALUE: !Ref ApprovalTagValue
          IGNORE_TAG_KEYS_CSV: !Ref IgnoreTagKeysCsv
          IGNORE_TAG_PAIRS_CSV: !Ref IgnoreTagPairsCsv
          REQUIRE_NO_ROUTE_REFERENCES: !Ref RequireNoRouteReferences
          INCLUDE_NAME_PREFIXES_CSV: !Ref IncludeNamePrefixesCsv
          EXCLUDE_NAME_PREFIXES_CSV: !Ref ExcludeNamePrefixesCsv
          MAX_NAT_GATEWAYS_PER_RUN: !Ref MaxNatGatewaysPerRun
          CONTINUE_ON_ACCESS_DENIED: !Ref ContinueOnAccessDenied
      Code:
        ZipFile: |
          import json
          import os
          import traceback
          from datetime import datetime, timezone, timedelta

          import boto3
          from botocore.exceptions import ClientError

          def utc_now():
              return datetime.now(timezone.utc)

          def utc_now_iso():
              return utc_now().isoformat()

          def env_bool(name, default=False):
              value = os.environ.get(name, str(default)).strip().lower()
              return value in ('1', 'true', 'yes', 'y')

          def env_int(name, default=0):
              try:
                  return int(os.environ.get(name, str(default)).strip())
              except Exception:
                  return default

          def split_csv(value):
              if value is None:
                  return []
              return [item.strip() for item in str(value).split(',') if item.strip()]

          def parse_tag_pairs(value):
              pairs = {}
              for item in split_csv(value):
                  if '=' not in item:
                      continue
                  key, val = item.split('=', 1)
                  key = key.strip()
                  val = val.strip()
                  if key:
                      pairs[key] = val
              return pairs

          def tags_to_dict(tags):
              result = {}
              for tag in tags or []:
                  key = tag.get('Key')
                  if key:
                      result[key] = tag.get('Value', '')
              return result

          def get_enabled_regions(excluded_regions):
              requested = os.environ.get('TARGET_REGIONS_CSV', 'all-enabled').strip()
              if requested and requested.lower() != 'all-enabled':
                  regions = split_csv(requested)
              else:
                  ec2 = boto3.client('ec2', region_name=os.environ.get('AWS_REGION', 'us-east-1'))
                  response = ec2.describe_regions(AllRegions=False)
                  regions = sorted([r['RegionName'] for r in response.get('Regions', [])])
              return [r for r in regions if r not in excluded_regions]

          def nat_age_days(nat):
              created = nat.get('CreateTime')
              if not created:
                  return None
              if created.tzinfo is None:
                  created = created.replace(tzinfo=timezone.utc)
              return (utc_now() - created).days

          def name_included(name, include_prefixes):
              if not include_prefixes:
                  return True
              return any(name.startswith(prefix) for prefix in include_prefixes)

          def name_excluded(name, exclude_prefixes):
              if not exclude_prefixes:
                  return False
              return any(name.startswith(prefix) for prefix in exclude_prefixes)

          def has_ignore_tag(tag_map, ignore_tag_keys, ignore_tag_pairs):
              for key in ignore_tag_keys:
                  if key in tag_map:
                      return True, 'ignore_tag_key=' + key
              for key, value in ignore_tag_pairs.items():
                  if tag_map.get(key) == value:
                      return True, 'ignore_tag_pair=' + key + '=' + value
              return False, ''

          def has_approval_tag(tag_map, settings):
              if not settings['require_approval_tag']:
                  return True
              return tag_map.get(settings['approval_tag_key']) == settings['approval_tag_value']

          def route_references_for_nat(ec2, nat_gateway_id):
              references = []
              paginator = ec2.get_paginator('describe_route_tables')
              for page in paginator.paginate():
                  for rt in page.get('RouteTables', []):
                      rt_id = rt.get('RouteTableId')
                      vpc_id = rt.get('VpcId')
                      for route in rt.get('Routes', []):
                          if route.get('NatGatewayId') == nat_gateway_id:
                              references.append({
                                  'route_table_id': rt_id,
                                  'vpc_id': vpc_id,
                                  'destination_cidr_block': route.get('DestinationCidrBlock'),
                                  'destination_ipv6_cidr_block': route.get('DestinationIpv6CidrBlock'),
                                  'state': route.get('State')
                              })
              return references

          def nat_metric_sum(cloudwatch, nat_gateway_id, vpc_id, metric_name, start, end):
              response = cloudwatch.get_metric_statistics(
                  Namespace='AWS/NATGateway',
                  MetricName=metric_name,
                  Dimensions=[
                      {'Name': 'NatGatewayId', 'Value': nat_gateway_id},
                      {'Name': 'VpcId', 'Value': vpc_id}
                  ],
                  StartTime=start,
                  EndTime=end,
                  Period=86400,
                  Statistics=['Sum']
              )
              return sum(float(point.get('Sum', 0.0)) for point in response.get('Datapoints', []))

          def nat_metric_summary(cloudwatch, nat, settings):
              nat_gateway_id = nat.get('NatGatewayId')
              vpc_id = nat.get('VpcId')
              days = max(1, int(settings['metrics_lookback_days']))
              end = utc_now()
              start = end - timedelta(days=days)
              metric_names = [
                  'BytesInFromSource',
                  'BytesOutToDestination',
                  'BytesInFromDestination',
                  'BytesOutToSource'
              ]
              totals = {}
              total_bytes = 0.0
              for metric in metric_names:
                  value = nat_metric_sum(cloudwatch, nat_gateway_id, vpc_id, metric, start, end)
                  totals[metric] = value
                  total_bytes += value
              return {
                  'lookback_days': days,
                  'total_bytes': total_bytes,
                  'daily_average_bytes': total_bytes / days,
                  'metrics': totals
              }

          def scan_region(region, settings):
              ec2 = boto3.client('ec2', region_name=region)
              cloudwatch = boto3.client('cloudwatch', region_name=region)
              result = {
                  'region': region,
                  'evaluated': 0,
                  'nat_gateways': 0,
                  'low_activity': 0,
                  'eligible': 0,
                  'would_delete': 0,
                  'deleted': 0,
                  'skipped_not_available': 0,
                  'skipped_too_new': 0,
                  'skipped_not_low_activity': 0,
                  'skipped_route_references': 0,
                  'skipped_include_prefix': 0,
                  'skipped_exclude_prefix': 0,
                  'skipped_missing_approval_tag': 0,
                  'skipped_ignore_tag': 0,
                  'access_denied': 0,
                  'failed': 0,
                  'failures': [],
                  'candidates': [],
                  'deleted_nat_gateways': []
              }

              try:
                  paginator = ec2.get_paginator('describe_nat_gateways')
                  pages = paginator.paginate()
              except ClientError as e:
                  code = e.response.get('Error', {}).get('Code', 'Unknown')
                  message = e.response.get('Error', {}).get('Message', str(e))
                  result['failures'].append({'operation': 'DescribeNatGateways', 'error_code': code, 'error_message': message})
                  if code in ('AccessDenied', 'AccessDeniedException', 'UnauthorizedOperation') or 'explicit deny' in message.lower():
                      result['access_denied'] += 1
                  else:
                      result['failed'] += 1
                  if not settings['continue_on_access_denied']:
                      raise
                  return result

              for page in pages:
                  for nat in page.get('NatGateways', []):
                      if settings['max_nat_gateways_per_run'] and (result['would_delete'] + result['deleted']) >= settings['max_nat_gateways_per_run']:
                          result['stopped_due_to_max_nat_gateways_per_run'] = True
                          return result

                      result['evaluated'] += 1
                      result['nat_gateways'] += 1
                      nat_gateway_id = nat.get('NatGatewayId')
                      state = nat.get('State')
                      vpc_id = nat.get('VpcId')
                      subnet_id = nat.get('SubnetId')
                      tag_map = tags_to_dict(nat.get('Tags', []))
                      name = tag_map.get('Name', '')
                      age_days = nat_age_days(nat)

                      base = {
                          'nat_gateway_id': nat_gateway_id,
                          'name': name,
                          'region': region,
                          'vpc_id': vpc_id,
                          'subnet_id': subnet_id,
                          'state': state,
                          'age_days': age_days,
                          'tags': tag_map
                      }

                      if not name_included(name, settings['include_name_prefixes']):
                          result['skipped_include_prefix'] += 1
                          continue

                      if name_excluded(name, settings['exclude_name_prefixes']):
                          result['skipped_exclude_prefix'] += 1
                          item = dict(base)
                          item['action'] = 'skipped_exclude_prefix'
                          result['candidates'].append(item)
                          continue

                      ignore, ignore_reason = has_ignore_tag(tag_map, settings['ignore_tag_keys'], settings['ignore_tag_pairs'])
                      if ignore:
                          result['skipped_ignore_tag'] += 1
                          item = dict(base)
                          item['action'] = 'skipped_ignore_tag'
                          item['reason'] = ignore_reason
                          result['candidates'].append(item)
                          continue

                      if state != 'available':
                          result['skipped_not_available'] += 1
                          continue

                      if age_days is None or age_days < settings['minimum_nat_gateway_age_days']:
                          result['skipped_too_new'] += 1
                          item = dict(base)
                          item['action'] = 'skipped_too_new'
                          result['candidates'].append(item)
                          continue

                      try:
                          metrics = nat_metric_summary(cloudwatch, nat, settings)
                      except ClientError as e:
                          code = e.response.get('Error', {}).get('Code', 'Unknown')
                          message = e.response.get('Error', {}).get('Message', str(e))
                          result['failures'].append({'nat_gateway_id': nat_gateway_id, 'operation': 'GetMetricStatistics', 'error_code': code, 'error_message': message})
                          if code in ('AccessDenied', 'AccessDeniedException') or 'explicit deny' in message.lower():
                              result['access_denied'] += 1
                          else:
                              result['failed'] += 1
                          if not settings['continue_on_access_denied']:
                              raise
                          continue

                      item = dict(base)
                      item['metric_summary'] = metrics

                      if metrics['daily_average_bytes'] >= settings['low_bytes_daily_average_threshold']:
                          result['skipped_not_low_activity'] += 1
                          item['action'] = 'skipped_not_low_activity'
                          result['candidates'].append(item)
                          continue

                      result['low_activity'] += 1

                      try:
                          route_refs = route_references_for_nat(ec2, nat_gateway_id)
                      except ClientError as e:
                          code = e.response.get('Error', {}).get('Code', 'Unknown')
                          message = e.response.get('Error', {}).get('Message', str(e))
                          result['failures'].append({'nat_gateway_id': nat_gateway_id, 'operation': 'DescribeRouteTables', 'error_code': code, 'error_message': message})
                          if code in ('AccessDenied', 'AccessDeniedException') or 'explicit deny' in message.lower():
                              result['access_denied'] += 1
                          else:
                              result['failed'] += 1
                          if not settings['continue_on_access_denied']:
                              raise
                          continue

                      item['route_references'] = route_refs

                      if settings['require_no_route_references'] and route_refs:
                          result['skipped_route_references'] += 1
                          item['action'] = 'skipped_route_references'
                          result['candidates'].append(item)
                          continue

                      if not has_approval_tag(tag_map, settings):
                          result['skipped_missing_approval_tag'] += 1
                          item['action'] = 'skipped_missing_approval_tag'
                          item['required_tag'] = settings['approval_tag_key'] + '=' + settings['approval_tag_value']
                          result['candidates'].append(item)
                          continue

                      result['eligible'] += 1

                      if settings['action_mode'] == 'review-only':
                          item['action'] = 'review_only_candidate'
                          result['candidates'].append(item)
                          continue

                      item['action'] = 'would_delete' if settings['dry_run'] else 'delete'

                      if settings['dry_run']:
                          result['would_delete'] += 1
                          result['candidates'].append(item)
                          continue

                      try:
                          ec2.delete_nat_gateway(NatGatewayId=nat_gateway_id)
                          result['deleted'] += 1
                          result['deleted_nat_gateways'].append(item)
                      except ClientError as e:
                          code = e.response.get('Error', {}).get('Code', 'Unknown')
                          message = e.response.get('Error', {}).get('Message', str(e))
                          result['failures'].append({'nat_gateway_id': nat_gateway_id, 'operation': 'DeleteNatGateway', 'error_code': code, 'error_message': message})
                          if code in ('AccessDenied', 'AccessDeniedException', 'UnauthorizedOperation') or 'explicit deny' in message.lower():
                              result['access_denied'] += 1
                          else:
                              result['failed'] += 1
                          if not settings['continue_on_access_denied']:
                              raise

              return result

          def handler(event, context):
              settings = {
                  'excluded_regions': split_csv(os.environ.get('EXCLUDED_REGIONS_CSV', '')),
                  'dry_run': env_bool('DRY_RUN', True),
                  'action_mode': os.environ.get('ACTION_MODE', 'review-only'),
                  'metrics_lookback_days': env_int('METRICS_LOOKBACK_DAYS', 7),
                  'low_bytes_daily_average_threshold': env_int('LOW_BYTES_DAILY_AVERAGE_THRESHOLD', 1048576),
                  'minimum_nat_gateway_age_days': env_int('MINIMUM_NAT_GATEWAY_AGE_DAYS', 30),
                  'require_approval_tag': env_bool('REQUIRE_APPROVAL_TAG', True),
                  'approval_tag_key': os.environ.get('APPROVAL_TAG_KEY', 'CostOptApproved'),
                  'approval_tag_value': os.environ.get('APPROVAL_TAG_VALUE', 'true'),
                  'ignore_tag_keys': split_csv(os.environ.get('IGNORE_TAG_KEYS_CSV', '')),
                  'ignore_tag_pairs': parse_tag_pairs(os.environ.get('IGNORE_TAG_PAIRS_CSV', '')),
                  'require_no_route_references': env_bool('REQUIRE_NO_ROUTE_REFERENCES', True),
                  'include_name_prefixes': split_csv(os.environ.get('INCLUDE_NAME_PREFIXES_CSV', '')),
                  'exclude_name_prefixes': split_csv(os.environ.get('EXCLUDE_NAME_PREFIXES_CSV', '')),
                  'max_nat_gateways_per_run': env_int('MAX_NAT_GATEWAYS_PER_RUN', 25),
                  'continue_on_access_denied': env_bool('CONTINUE_ON_ACCESS_DENIED', True),
              }

              if isinstance(event, dict):
                  if 'dry_run' in event:
                      settings['dry_run'] = bool(event['dry_run'])
                  if 'action_mode' in event:
                      settings['action_mode'] = str(event['action_mode'])
                  if 'require_approval_tag' in event:
                      settings['require_approval_tag'] = bool(event['require_approval_tag'])
                  if 'metrics_lookback_days' in event:
                      settings['metrics_lookback_days'] = int(event['metrics_lookback_days'])
                  if 'low_bytes_daily_average_threshold' in event:
                      settings['low_bytes_daily_average_threshold'] = int(event['low_bytes_daily_average_threshold'])
                  if 'minimum_nat_gateway_age_days' in event:
                      settings['minimum_nat_gateway_age_days'] = int(event['minimum_nat_gateway_age_days'])
                  if 'require_no_route_references' in event:
                      settings['require_no_route_references'] = bool(event['require_no_route_references'])
                  if 'include_name_prefixes' in event:
                      settings['include_name_prefixes'] = list(event['include_name_prefixes'])
                  if 'exclude_name_prefixes' in event:
                      settings['exclude_name_prefixes'] = list(event['exclude_name_prefixes'])
                  if 'max_nat_gateways_per_run' in event:
                      settings['max_nat_gateways_per_run'] = int(event['max_nat_gateways_per_run'])
                  if 'target_regions' in event and event['target_regions']:
                      os.environ['TARGET_REGIONS_CSV'] = ','.join(event['target_regions'])
                  if 'excluded_regions' in event and event['excluded_regions']:
                      settings['excluded_regions'] = list(event['excluded_regions'])

              if settings['action_mode'] not in ('review-only', 'delete'):
                  settings['action_mode'] = 'review-only'

              summary = {
                  'started_at': utc_now_iso(),
                  'dry_run': settings['dry_run'],
                  'action_mode': settings['action_mode'],
                  'metrics_lookback_days': settings['metrics_lookback_days'],
                  'low_bytes_daily_average_threshold': settings['low_bytes_daily_average_threshold'],
                  'minimum_nat_gateway_age_days': settings['minimum_nat_gateway_age_days'],
                  'require_approval_tag': settings['require_approval_tag'],
                  'approval_tag': settings['approval_tag_key'] + '=' + settings['approval_tag_value'],
                  'require_no_route_references': settings['require_no_route_references'],
                  'include_name_prefixes': settings['include_name_prefixes'],
                  'exclude_name_prefixes': settings['exclude_name_prefixes'],
                  'ignore_tag_keys': settings['ignore_tag_keys'],
                  'ignore_tag_pairs': settings['ignore_tag_pairs'],
                  'max_nat_gateways_per_run': settings['max_nat_gateways_per_run'],
                  'excluded_regions': settings['excluded_regions'],
                  'regions': [],
                  'totals': {
                      'evaluated': 0,
                      'nat_gateways': 0,
                      'low_activity': 0,
                      'eligible': 0,
                      'would_delete': 0,
                      'deleted': 0,
                      'skipped_not_available': 0,
                      'skipped_too_new': 0,
                      'skipped_not_low_activity': 0,
                      'skipped_route_references': 0,
                      'skipped_include_prefix': 0,
                      'skipped_exclude_prefix': 0,
                      'skipped_missing_approval_tag': 0,
                      'skipped_ignore_tag': 0,
                      'access_denied': 0,
                      'failed': 0
                  },
                  'region_results': []
              }

              try:
                  regions = get_enabled_regions(settings['excluded_regions'])
                  summary['regions'] = regions
                  for region in regions:
                      try:
                          region_result = scan_region(region, settings)
                      except Exception as e:
                          region_result = {
                              'region': region,
                              'evaluated': 0,
                              'nat_gateways': 0,
                              'low_activity': 0,
                              'eligible': 0,
                              'would_delete': 0,
                              'deleted': 0,
                              'skipped_not_available': 0,
                              'skipped_too_new': 0,
                              'skipped_not_low_activity': 0,
                              'skipped_route_references': 0,
                              'skipped_include_prefix': 0,
                              'skipped_exclude_prefix': 0,
                              'skipped_missing_approval_tag': 0,
                              'skipped_ignore_tag': 0,
                              'access_denied': 0,
                              'failed': 1,
                              'failures': [{'error_code': type(e).__name__, 'error_message': str(e), 'trace': traceback.format_exc()}],
                              'candidates': [],
                              'deleted_nat_gateways': []
                          }
                      summary['region_results'].append(region_result)
                      for key in summary['totals']:
                          summary['totals'][key] += int(region_result.get(key, 0) or 0)
              finally:
                  summary['ended_at'] = utc_now_iso()
                  print(json.dumps(summary, indent=2, default=str))
              return summary

  RemediatorScheduleRule:
    Type: AWS::Events::Rule
    Condition: IsScheduleEnabled
    Properties:
      Name: !Sub '${ProjectName}-schedule'
      Description: Scheduled run for NAT Gateway Idle Review Remediator.
      ScheduleExpression: !Ref ScheduleExpression
      State: ENABLED
      Targets:
        - Arn: !GetAtt RemediatorFunction.Arn
          Id: NatGatewayIdleReviewRemediatorTarget
          Input: '{"source":"eventbridge-schedule","dry_run":true,"action_mode":"review-only"}'

  RemediatorSchedulePermission:
    Type: AWS::Lambda::Permission
    Condition: IsScheduleEnabled
    Properties:
      FunctionName: !Ref RemediatorFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt RemediatorScheduleRule.Arn

Outputs:
  RemediatorFunctionName:
    Description: Lambda function name.
    Value: !Ref RemediatorFunction

  RemediatorFunctionArn:
    Description: Lambda function ARN.
    Value: !GetAtt RemediatorFunction.Arn

  DryRunSetting:
    Description: Current DryRun setting.
    Value: !Ref DryRun

  ActionModeSetting:
    Description: Current ActionMode setting.
    Value: !Ref ActionMode

  ManualReviewExample:
    Description: Example manual review invocation payload.
    Value: !Sub 'aws lambda invoke --function-name ${RemediatorFunction} --cli-binary-format raw-in-base64-out --payload ''{"dry_run":true,"action_mode":"review-only","require_approval_tag":false}'' response.json'

  ManualDeleteDryRunExample:
    Description: Example delete dry-run invocation payload.
    Value: !Sub 'aws lambda invoke --function-name ${RemediatorFunction} --cli-binary-format raw-in-base64-out --payload ''{"dry_run":true,"action_mode":"delete","require_approval_tag":true}'' response.json'
